﻿using System;
using PgBirdie.Core.Config;
using PgBirdie.Core.Extensions;
using PgBirdie.Core.Model;
using PgBirdie.Core.Model.Database.Data;
using PgBirdie.Core.Model.Database.Structure;
using PgBirdie.Core.Model.Database.Structure.ColumnTypes.DateTime;
using PgBirdie.Core.Model.Database.Structure.ColumnTypes.Text;
using PgBirdie.Core.Service.DbObject;

namespace PgBirdie.Core.Service.Compare
{
    public class CompareService : ICompareService
    {
        private readonly IDbObjectFromDbService _dbObjectFromDb;

        public CompareService(IDbObjectFromDbService dbObjectFromDb)
        {
            _dbObjectFromDb = dbObjectFromDb;
        }

        public bool TablesHaveDifferences(PgDatabase source, PgDatabase target)
        {
            if (source == null || target == null)
                throw new Exception("Cannot compare tables if either source or target db is null.");

            if (source.Schemas.Count != target.Schemas.Count)
                return true;

            foreach (var schema in source.Schemas.Values)
            {
                if (!target.Schemas.ContainsKey(schema.Name))
                    return true;

                var remoteTables = target.Schemas[schema.Name].Tables;

                if (schema.Tables.Count != remoteTables.Count)
                    return true;

                foreach (var table in schema.Tables.Values)
                {
                    if (!remoteTables.ContainsKey(table.Name))
                        return true;

                    var remoteTable = remoteTables[table.Name];

                    if (!table.PrimaryKey.Name.Equals(remoteTable.PrimaryKey.Name))
                        return true;

                    foreach (var key in table.UniqueKeys)
                    {
                        if (!remoteTable.UniqueKeys.ContainsKey(key.Key))
                            return true;
                    }

                    foreach (var key in table.ForeignKeys)
                    {
                        if (!remoteTable.ForeignKeys.ContainsKey(key.Key))
                            return true;
                    }

                    if (table.Indexes.Count != remoteTable.Indexes.Count)
                        return true;

                    foreach (var index in table.Indexes)
                    {
                        if (!remoteTable.Indexes.ContainsKey(index.Key))
                            return true;
                    }

                    if (table.Columns.Count != remoteTable.Columns.Count)
                        return true;

                    foreach (var column in table.Columns.Values)
                    {
                        if (!remoteTable.Columns.ContainsKey(column.Name))
                            return true;

                        var remoteColumn = remoteTable.Columns[column.Name];

                        if (!ColumnPropertiesMatch(column, remoteColumn))
                            return true;
                    }

                    var remoteColumns = remoteTables[table.Name].Columns;
                    foreach (var column in table.Columns)
                    {
                        if (!remoteColumns.ContainsKey(column.Key))
                            return true;
                    }
                }
            }

            return false;
        }

        public bool StaticDataHaveDifferences(PgDatabase source, PgDatabase target)
        {
            if (source == null || target == null)
                throw new Exception("Cannot compare static data if either source or target db is null.");

            if (source.Schemas.IsNullOrEmpty() || target.Schemas.IsNullOrEmpty())
                return false;

            foreach (var schema in source.Schemas.Values)
            {
                if (!target.Schemas.ContainsKey(schema.Name))
                    continue;

                foreach (var table in schema.Tables.Values)
                {
                    if (!target.Schemas[schema.Name].Tables.ContainsKey(table.Name) &&
                        Settings.Source.Schemas[schema.Name].Tables[table.Name].WithData)
                        return true;

                    if (!target.Schemas[schema.Name].Tables.ContainsKey(table.Name))
                        continue;

                    var targetTable = target.Schemas[schema.Name].Tables[table.Name];

                    foreach (var dataRow in table.DataRows.Values)
                    {
                        if (!targetTable.DataRows.ContainsKey(dataRow.Uid))
                            return true;

                        var targetDataRow = targetTable.DataRows[dataRow.Uid];

                        foreach (var dataColumn in dataRow.DataColumns)
                        {
                            if (!targetDataRow.DataColumns.ContainsKey(dataColumn.Key))
                                continue;

                            if (ExcludeColumn(dataColumn.Value))
                                continue;

                            var targetDataColumn = targetDataRow.DataColumns[dataColumn.Key];

                            if (!dataColumn.Value.Value.Equals(targetDataColumn.Value, StringComparison.Ordinal))
                                return true;
                        }
                    }
                }                
            }

            return false;
        }

        public bool SequencesHaveDifferences(PgDatabase source, PgDatabase target)
        {
            if (source == null || target == null)
                throw new Exception("Cannot compare sequences if either source or target db is null.");

            if (source.Schemas.Count != target.Schemas.Count)
                return true;

            foreach (var schema in source.Schemas.Values)
            {
                if (!target.Schemas.ContainsKey(schema.Name))
                    return true;

                var remoteSequences = target.Schemas[schema.Name].Sequences;

                if (schema.Sequences.Count != remoteSequences.Count)
                    return true;

                foreach (var sequence in schema.Sequences.Values)
                {
                    if (!remoteSequences.ContainsKey(sequence.Name))
                        return true;

                    var remoteSequence = remoteSequences[sequence.Name];

                    if (sequence.IncrementBy != remoteSequence.IncrementBy)
                        return true;

                    if (sequence.MinValue != remoteSequence.MinValue)
                        return true;

                    if (sequence.MaxValue != remoteSequence.MaxValue)
                        return true;

                    if (sequence.Cache != remoteSequence.Cache)
                        return true;
                }
            }

            return false;
        }

        public bool AccessControlHasDifferences(PgDatabase source, PgDatabase target)
        {
            return OwnersHaveDifferences(source, target) || PrivilegesHaveDifferences(source, target);
        }

        public bool OwnersHaveDifferences(PgDatabase source, PgDatabase target)
        {
            var sourceOwners = _dbObjectFromDb.GetAllOwners(source);
            var targetOwners = _dbObjectFromDb.GetAllOwners(target);

            if (sourceOwners.Count != targetOwners.Count)
                return true;

            foreach (var owner in targetOwners)
                if (!sourceOwners.ContainsKey(owner.Key))
                    return true;

            return false;
        }

        public bool PrivilegesHaveDifferences(PgDatabase source, PgDatabase target)
        {
            var sourcePrivs = _dbObjectFromDb.GetAllPrivileges(source);
            var targetPrivs = _dbObjectFromDb.GetAllPrivileges(target);
   
            if (sourcePrivs.Count != targetPrivs.Count)
                return true;

            foreach (var priv in targetPrivs)
                if (!sourcePrivs.ContainsKey(priv.Key))
                    return true;

            return false;
        }

        public bool ColumnPropertiesMatch(PgColumn source, PgColumn target)
        {
            if (source == null || target == null)
                return false;

            return ColumnTypePropertiesMatch(source, target) &&
                   ColumnTableNameMatch(source, target) &&
                   ColumnDataTypeMatch(source, target) &&
                   ColumnIsNullableMatch(source, target) &&
                   ColumnSequenceMatch(source, target);
        }

        public bool ColumnTypePropertiesMatch(PgColumn source, PgColumn target)
        {
            if (source == null || target == null)
                return false;

            if (source.Type.AsEnum == ColumnDataType.Char || source.Type.AsEnum == ColumnDataType.VarChar)
            {
                var srcType = source.Type as PgCharacterBase;
                var trgType = target.Type as PgCharacterBase;
                if (srcType == null || trgType == null)
                    return false;
                if (srcType.Length != trgType.Length)
                    return false;
            }

            if (source.Type.AsEnum == ColumnDataType.Timestamp)
            {
                var srcType = source.Type as PgTimestamp;
                var trgType = target.Type as PgTimestamp;
                if (srcType == null || trgType == null)
                    return false;
                if (srcType.WithTimeZone != trgType.WithTimeZone ||
                    srcType.Precision != trgType.Precision)
                    return false;
            }

            if (source.Type.AsEnum != target.Type.AsEnum)
                return false;

            return true;
        }

        public bool ColumnTableNameMatch(PgColumn source, PgColumn target)
        {
            if (source == null || target == null)
                return false;
            return source.Table.Name.Equals(target.Table.Name);
        }

        public bool ColumnDataTypeMatch(PgColumn source, PgColumn target)
        {
            if (source == null || target == null)
                return false;
            return source.Type.AsEnum == target.Type.AsEnum;
        }        

        public bool ColumnIsNullableMatch(PgColumn source, PgColumn target)
        {
            if (source == null || target == null)
                return false;
            return source.IsNullable == target.IsNullable;
        }

        public bool ColumnSequenceMatch(PgColumn source, PgColumn target)
        {
            if (source == null || target == null)
                return false;
            return source.SequenceName == target.SequenceName;
        }

        private bool ExcludeColumn(PgDataColumn dataColumn)
        {
            var column = dataColumn;
            var names = Settings.Source.Schemas[column.Table.Schema.Name].Tables[column.Table.Name].ExcludeStaticDataColumnNames;
            if (names.IsNullOrEmpty())
                return false;

            if (names.Contains(column.Name))
                return true;

            return false;
        }
    }
}